package battleship;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.sql.SQLException;
import java.util.Random;

public class Game implements Serializable {
	private static final long serialVersionUID = 1L;

	final public static Ship[] ships = { new Ship("patrol boat", 2),
			new Ship("submarine", 3), new Ship("destroyer", 3),
			new Ship("battleship", 4), new Ship("aircraft carrier", 5) };
	final public static String SERIAL_PATH = "./";
	final public static int BOARD_LENGTH = 10;

	/** The database key for this object. */
	final public int gid;
	/** pids[i] = uid of player (i+1). */
	final private String[] pids;
	/** playerShips[i] = array of ships for player (i+1). */
	final private PlayerShip[][] playerShips;
	/**
	 * attacked[i][y][x] == true iff player (i+1) has attacked their enemy at
	 * (x, y).
	 */
	final private boolean[][][] attacked;
	/** doneSetup[i] == true iff player (i+1) completed the setup phase. */
	final private boolean[] doneSetup;
	/** The ID of the player who currently has their turn. */
	private String turnId;
	/** The winner of the game, when finished. */
	private String winner;
	/** lastMove[i] = coordinates of the last move made by player (i+1). */
	final private int[][] lastMove;
	/** msg[i] = the message to be displayed for player (i+1) */
	private String[] msg;
	/**
	 * Flag that is true iff something significant happened since the last
	 * serialization.
	 */
	private boolean changesMade;

	public Game(int id, String p1id, String p2id) {
		this.gid = id;
		pids = new String[] { p1id, p2id };

		attacked = new boolean[2][][];
		for (int i = 0; i < attacked.length; i++) {
			attacked[i] = new boolean[BOARD_LENGTH][];
			for (int y = 0; y < BOARD_LENGTH; y++) {
				attacked[i][y] = new boolean[BOARD_LENGTH];
				for (int x = 0; x < BOARD_LENGTH; x++) {
					attacked[i][y][x] = false;
					attacked[i][y][x] = false;
				}
			}
		}

		doneSetup = new boolean[pids.length];
		lastMove = new int[pids.length][2];
		msg = new String[pids.length];
		playerShips = new PlayerShip[pids.length][ships.length];
		for (int i = 0; i < doneSetup.length; i++) {
			doneSetup[i] = false;
			msg[i] = "";
			for (int j = 0; j < lastMove[i].length; j++) {
				lastMove[i][j] = -1;
			}
		}
		
		if (p2id.equals(Constants.COMPUTER_UID)) {
			final Random generator = new Random();
			while (!validateSetup(p2id)) {
				for (final Ship s : Game.ships) {
					final int x = generator.nextInt(10);
					final int y = generator.nextInt(10);
					final boolean vertical = generator.nextBoolean();
					addShip(p2id, s.name, x, y, vertical);
				}
			}
		}

		winner = null;
		turnId = pids[0];
		changesMade = true;
	}

	/**
	 * Insert a ship into the grid during the setup phase.
	 * 
	 * @param pid
	 *            the ID of the player making the insertion.
	 * @param name
	 *            the name of the ship being inserted.
	 * @param x
	 *            the x-value of the ship's top-left corner.
	 * @param y
	 *            the y-value of the ship's top-left corner.
	 * @param vertical
	 *            true iff the ship is to be facing east.
	 */
	public void addShip(String pid, String name, int x, int y, boolean vertical) {
		for (int i = 0; i < pids.length; i++) {
			if (pid.equals(pids[i]) && !doneSetup[i]) {
				try {
					for (int j = 0; j < ships.length; j++) {
						if (name.equals(ships[j].name)) {
							playerShips[i][j] = new PlayerShip(ships[j], x, y,
									vertical);
							break;
						}
					}
				} catch (NumberFormatException nfe) {
				} catch (ArrayIndexOutOfBoundsException aioobe) {
				}
			}
		}
	}

	/**
	 * Attempt to execute a player's move and return true iff successful.
	 * 
	 * @param pid
	 *            the ID of the player moving.
	 * @param x
	 *            the x-coordinate of the attack.
	 * @param y
	 *            the y-coordinate of the attack.
	 * @throws SQLException
	 */
	public boolean attack(String pid, int x, int y) throws SQLException {
		for (int i = 0; i < pids.length; i++) {
			if (pid.equals(pids[i])) {
				if (!turnId.equals(pid)) {
				} else if (attacked[i][y][x]) {
					// msg[i] = "You already attacked that location.";
					break;
				} else {
					attacked[i][y][x] = true;
					lastMove[i][0] = x;
					lastMove[i][1] = y;
					msg[i] = "";

					final int j = (i + 1) % pids.length;
					for (int k = 0; k < playerShips[j].length; k++) {
						if (playerShips[j][k].isHit(x, y)) {
							msg[i] = "<span style=\"color: red;\">";
							msg[j] = "<span style=\"color: red;\">";
							if (playerShips[j][k].getHealth() == 0) {
								// Ship has sunk
								msg[i] += "We sunk their "
										+ playerShips[j][k].s.name + "!";
								msg[j] += "They sunk our "
										+ playerShips[j][k].s.name + "!";

								// Check if game is over
								boolean stillAlive = false;
								for (k = 0; k < playerShips[j].length; k++) {
									if (playerShips[j][k].getHealth() > 0) {
										stillAlive = true;
										break;
									}
								}
								if (!stillAlive) {
									winner = pids[i];
									msg[i] += "<br>You win!";
									msg[j] += "<br>You lose!";
								}
							} else {
								msg[i] += "We hit something!";
								msg[j] += "They struck our "
										+ playerShips[j][k].s.name + "!";
							}
							msg[i] += "</span>";
							msg[j] += "</span>";
							break;
						} else {
							msg[i] = "We missed";
							msg[j] = "They missed";
						}
					}

					if (msg[i].equals("")) {
						msg[i] = "We missed";
					}

					turnId = pids[j];
					changesMade = true;
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Return true iff the player has already attacked at the given coordinate
	 * 
	 * @param pid
	 *            the ID of the player.
	 * @param x
	 *            the x-coordinate of the attack.
	 * @param y
	 *            the y-coordinate of the attack.
	 * @return true iff the player has already attacked at this coordinate
	 */
	public boolean checkAttack(String pid, int x, int y) {
		int playerInt = -1;
		for (int i = 0; i < pids.length; i++) {
			if (pid.equals(pids[i])) {
				playerInt = i;
				break;
			}
		}
		try {
			return attacked[playerInt][y][x];
		} catch (ArrayIndexOutOfBoundsException e) {
			return true;
		}
	}
	
	/**
	 * Return a message to be displayed for the given player.
	 * 
	 * @param pid
	 *            the ID of the player.
	 * @return a message to be displayed for the given player.
	 */
	public String getMessage(String pid) {
		for (int i = 0; i < pids.length; i++) {
			if (pid.equals(pids[i])) {
				return msg[i];
			}
		}
		return null;
	}

	/**
	 * Return the HTML representation of the grid of the given player's
	 * opponent.
	 * 
	 * @param pid
	 *            the ID of the player.
	 * @return the HTML representation of the opponent's grid.
	 */
	public String getEnemyGrid(String pid) {
		for (int i = 0; i < pids.length; i++) {
			if (pid.equals(pids[i])) {
				return getGridTable((i + 1) % pids.length, "setCoords");
			}
		}
		return null;
	}

	/**
	 * Return the ships of the opponent of the player with the given ID.
	 * 
	 * @param pid
	 *            the ID of a player.
	 * @return the ships of the player's opponent.
	 */
	public PlayerShip[] getEnemyShips(String pid) {
		for (int i = 0; i < pids.length; i++) {
			if (pid.equals(pids[i])) {
				return playerShips[(i + 1) % pids.length];
			}
		}
		return null;
	}

	/**
	 * Return the HTML representation of player (i+1)'s grid.
	 * 
	 * @param i
	 *            the player's number minus 1.
	 * @return the HTML representation of the player's grid.
	 */
	private String getGridTable(int i, String onclick) {
		final int j = (i + 1) % pids.length;
		String rv = "<table class=\"battlegrid\">\n<tr><th></th>\n";
		for (int x = 0; x < BOARD_LENGTH; x++) {
			rv += String
					.format("<th style=\"position: absolute; left: %dpx; top: %dpx;\">",
							(x + 1) * GameBean.CELL_SIZE + GameBean.OFFSET_X,
							GameBean.OFFSET_Y)
					+ ((char) (65 + x)) + "</th>\n";
		}
		rv += "</tr>";
		for (int y = 0; y < attacked[j].length; y++) {
			rv += "<tr>\n";
			rv += String.format(
					"<th style=\"position: absolute; left: %dpx; top: %dpx\">",
					GameBean.OFFSET_X, (y + 1) * GameBean.CELL_SIZE
							+ GameBean.OFFSET_Y)
					+ y + "</th>\n";
			for (int x = 0; x < attacked[j][y].length; x++) {
				String cssClass = "";
				if (lastMove[j][0] == x && lastMove[j][1] == y) {
					cssClass += "lastmove";
				}
				boolean occupied = false;
				label: for (final PlayerShip ps : playerShips[i]) {
					if (ps != null) {
						for (int[] coords : ps.getOccupiedCoords()) {
							if (coords[0] == x && coords[1] == y) {
								occupied = true;
								break label;
							}
						}
					}
				}

				if (attacked[j][y][x]) {
					cssClass += occupied ? "hit" : "miss";
				}

				rv += String
						.format("<td style=\"position: absolute; left: %dpx; top: %dpx;\"",
								(x + 1) * GameBean.CELL_SIZE
										+ GameBean.OFFSET_X, (y + 1)
										* GameBean.CELL_SIZE
										+ GameBean.OFFSET_Y);
				if (!cssClass.equals("")) {
					rv += " class=\"" + cssClass + '"';
				}
				rv += " title=\"" + ((char) (65 + x)) + y + '"';
				if (!attacked[j][y][x]) {
					rv += String.format(" onClick=\"%s(this)\"", onclick);
				}
				rv += "></td>\n";
			}
			rv += "</tr>\n";
		}
		return rv + "</table>\n";
	}

	/**
	 * Return the HTML representation of the given player's grid.
	 * 
	 * @param pid
	 *            the ID of the player.
	 * @return the HTML representation of the given player's grid.
	 */
	public String getMyGrid(String pid) {
		for (int i = 0; i < pids.length; i++) {
			if (pid.equals(pids[i])) {
				return getGridTable(i, "deployShip");
			}
		}
		return null;
	}

	/**
	 * Return the ships of the player with the given ID.
	 * 
	 * @param pid
	 *            the ID of a player.
	 * @return the ships of the player.
	 */
	public PlayerShip[] getMyShips(String pid) {
		for (int i = 0; i < pids.length; i++) {
			if (pid.equals(pids[i])) {
				return playerShips[i];
			}
		}
		return null;
	}

	/**
	 * Return the IDs of the participants.
	 * 
	 * @return the IDs of the participants.
	 */
	public String[] getPids() {
		return pids;
	}

	/**
	 * Return the time at which the Game object was last modified.
	 * 
	 * @return the time at which the Game object was last modified.
	 */
	public long getTimestamp() {
		return getTimestamp(String.valueOf(gid));
	}

	public static long getTimestamp(String gid) {
		final File f = new File(SERIAL_PATH + gid);
		if (f.exists()) {
			return f.lastModified();
		}
		return -1;
	}

	/**
	 * Return the ID of the player who has the turn.
	 * 
	 * @return the ID of the player.
	 */
	public String getTurn() {
		return turnId;
	}

	/**
	 * Return the ID of the winner if the game is finished, null otherwise.
	 * 
	 * @return the ID of the winner if the game is finished, null otherwise;
	 */
	public String getWinner() {
		return winner;
	}

	/**
	 * Return true iff the given player's opponent has completed the setup
	 * phase.
	 * 
	 * @param pid
	 *            the ID of the player.
	 * @return true iff the given player's opponent has completed the setup
	 *         phase.
	 */
	public boolean opponentSetup(String pid) {
		for (int i = 0; i < pids.length; i++) {
			if (pid.equals(pids[i])) {
				return doneSetup[(i + 1) % pids.length];
			}
		}
		return false;
	}

	/**
	 * Verify that the ships the player attempted to add are within the board's
	 * dimensions and do not overlap each other.
	 * 
	 * @param pid
	 *            The player ID.
	 * @return
	 */
	public boolean validateSetup(String pid) {
		for (int i = 0; i < pids.length; i++) {
			if (pid.equals(pids[i])) {
				if (doneSetup[i]) {
					return true;
				}

				doneSetup[i] = true;
				boolean[][] occupied = new boolean[BOARD_LENGTH][BOARD_LENGTH];
				try {
					for (int j = 0; j < playerShips[i].length; j++) {
						if (playerShips[i][j] == null) {
							doneSetup[i] = false;
							break;
						}
						int[][] shipPos = playerShips[i][j].getOccupiedCoords();
						for (int k = 0; k < shipPos.length; k++) {
							int x = shipPos[k][0], y = shipPos[k][1];
							if (occupied[y][x]) {
								doneSetup[i] = false;
								break;
							} else if (x < 0 || x >= BOARD_LENGTH || y < 0
									|| y >= BOARD_LENGTH) {
								doneSetup[i] = false;
								break;
							}
							occupied[y][x] = true;
						}
					}
				} catch (ArrayIndexOutOfBoundsException e) {
					doneSetup[i] = false;
				}

				if (doneSetup[i]) {
					msg[i] = "";
					changesMade = true;
				} else {
					// Remove the ships that the player added
					for (int j = 0; j < playerShips[i].length; j++) {
						playerShips[i][j] = null;
					}
				}
				return doneSetup[i];
			}
		}
		return false;
	}

	/**
	 * Serialize the Game object if something has changed since being
	 * unserialised.
	 * 
	 * @throws FileNotFoundException
	 * @throws IOException
	 */
	public void serializeIfChanged() throws FileNotFoundException, IOException {
		if (changesMade) {
			changesMade = false;
			FileOutputStream fout = new FileOutputStream(SERIAL_PATH + gid);
			ObjectOutputStream oos = new ObjectOutputStream(fout);
			oos.writeObject(this);
			oos.close();
		}
	}

	/**
	 * Return the Game object with the given ID.
	 * 
	 * @param gid
	 *            the ID of the Game.
	 * @return a Game object.
	 */
	public static Game unserialize(String gid) throws ClassNotFoundException,
			IOException {
		FileInputStream fin = new FileInputStream(SERIAL_PATH + gid);
		ObjectInputStream ois = new ObjectInputStream(fin);
		final Game g = (Game) ois.readObject();
		ois.close();
		return g;
	}
}